﻿
using Microsoft.Office.Interop.OneNote;
using System.Xml.Linq;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;

namespace WetHatLab.OneNote.TaggingKit.edit
{
    /// <summary>
    /// Local representation of a OneNote Page
    /// </summary>
    /// <remarks>Supports tag related operations.</remarks>
    class OneNotePageProxy
    {
        // Sequence of elements below the page tag
        //<xsd:element name="TagDef" type="TagDef" minOccurs="0" maxOccurs="unbounded"/>
        //<xsd:element name="QuickStyleDef" type="QuickStyleDef" minOccurs="0" maxOccurs="unbounded"/>
        //<xsd:element name="XPSFile" type="XPSFile" minOccurs="0" maxOccurs="unbounded"/>
        //<xsd:element name="Meta" type="Meta" minOccurs="0" maxOccurs="unbounded"/>
        //<xsd:element name="MediaPlaylist" type="MediaPlaylist" minOccurs="0"/>
        //<xsd:element name="MeetingInfo" type="MeetingInfo" minOccurs="0"/>
        //<xsd:element name="PageSettings" type="PageSettings" minOccurs="0"/>
        //<xsd:element name="Title" type="Title" minOccurs="0"/>
        private static readonly String[] ELEMENT_SEQUENCE = { "TagDef", "QuickStyleDef", "XPSFile", "Meta", "MediaPlaylist", "MeetingInfo", "PageSettings", "Title" };
        private const int TAGDEF_IDX = 0;
        private const int QUICKSTYLEDEF_IDX = 1;
        private const int META_IDX = 3;
        private const int TITLE_IDX = 7;

        internal static readonly String META_NAME = "TaggingKit.PageTags";

        private bool _updateRequired = false;
        private XMLSchema _schema;
        // the OneNote page document
        private XDocument _pageDoc;

        // the onenote application object
        private Application _onenote;

        // <one:T> element with tags
        private XElement _pageTagsOE = null;

        XElement _page;
        XNamespace _one;

        internal OneNotePageProxy(Application onenoteApp, string pageID, XMLSchema schema)
        {
            _onenote = onenoteApp;
            _schema = schema;
            // Get the page XML
            string strPageContent;
            _onenote.GetPageContent(pageID, out strPageContent, PageInfo.piBasic, schema);

            _pageDoc = XDocument.Parse(strPageContent);
            _one = _pageDoc.Root.GetNamespaceOfPrefix("one");
            _page = _pageDoc.Root;

            XName outlineName = _one.GetName("Outline");

            // find the tags <one:TagDef> element
            // <one:TagDef index="0" name="Tags" type="23" symbol="26" />
            XElement tagDef = (from d in _page.Elements(_page.GetNamespaceOfPrefix("one").GetName("TagDef"))
                               where d.Attribute("name").Value == "Page Tags" && d.Attribute("type").Value == "23" && d.Attribute("symbol").Value == "26"
                               select d).FirstOrDefault();

            // For performance reasons we are going to delete all outlines not related to tags
            // Note: Page updates will actually leave those removed outlines on the page.
            List<XElement> outlinesToDelete = new List<XElement>();
            
            // find or create the <one:Outline> containing page tags.
            if (tagDef != null)
            {
                string defindex = tagDef.Attribute("index").Value;
                // locate the tag outline element looking like this
                //<one:Outline author="Peter Ernst" authorInitials="PE" lastModifiedBy="Peter Ernst" lastModifiedByInitials="PE" objectID="{E470786C-A904-4E9F-AC3B-0D9F36B6FC54}{14}{B0}" lastModifiedTime="2013-12-06T16:04:48.000Z">
                //  <one:Position x="236.249984741211" y="42.1500015258789" z="0" />
                //  <one:Size width="90.8847274780273" height="10.9862976074219" />
                //  <one:OEChildren>
                //    <one:OE objectID="{E470786C-A904-4E9F-AC3B-0D9F36B6FC54}{15}{B0}" lastModifiedTime="2013-12-06T16:04:48.000Z" quickStyleIndex="1" creationTime="2013-12-06T16:03:50.000Z">
                //      <one:Tag index="0" completed="true" creationDate="2013-12-06T15:55:59.000Z" completionDate="2013-12-06T15:55:59.000Z" />
                //      <one:T><![CDATA[Gdfgdf, sdfdsf]]></one:T>
                //    </one:OE>
                //  </one:OEChildren>
                //</one:Outline>
                //
                // For performance reasons we avoid Xpath!
                foreach (var outline in _page.Elements(outlineName))
                {
                    // the outline we are looking for has one <one:OEChildren> element with one <one:OE> element containing
                    // a <one:Tag> with the given index
                    if (_pageTagsOE == null)
                    {
                        XElement OEChildren = outline.Elements(_page.GetNamespaceOfPrefix("one").GetName("OEChildren")).FirstOrDefault();
                        if (OEChildren != null)
                        {
                            XElement OE = OEChildren.Elements(_page.GetNamespaceOfPrefix("one").GetName("OE")).FirstOrDefault();
                            if (OE != null)
                            {
                                XElement tag = (from t in OE.Elements(_page.GetNamespaceOfPrefix("one").GetName("Tag"))
                                                where t.Attribute("index").Value == defindex
                                                select t).FirstOrDefault();
                                if (tag != null)
                                { // found the outline with page tags, get the text element containing the tags
                                    _pageTagsOE = OE;
                                    continue;
                                }
                            }
                        }
                    }
                    outlinesToDelete.Add(outline);
                }
            }
            else
            {
                // record all outline elements for deletion
                foreach (var outline in _page.Elements(outlineName))
                {
                    outlinesToDelete.Add(outline);
                }
            }

            // delete outlines to make updating the page as fast as possible, knowing that
            // the update method will keep these outlines actually on the page.
            foreach (XElement outline in outlinesToDelete)
            {
                outline.Remove();
            }
        }

        internal static string[] ParseTags(string tags)
        {
            if (!string.IsNullOrEmpty(tags))
            {
                // remove al HTML markup
                tags = Regex.Replace(tags, "<[^<>]+>", String.Empty);
                string[] parsedTags = tags.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                // normalize tags
                for (int i = 0; i < parsedTags.Length; i++)
                {
                    parsedTags[i] = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(parsedTags[i].Trim());
                }
                return parsedTags;
            }
            return new string[0];
        }

        /// <summary>
        /// Add a new element at the correct position to a page
        /// </summary>
        /// <param name="element">element to add</param>
        /// <param name="sequence">index of element name in the schema sequence</param>
        private void addElementToPage(XElement element, int sequence)
        {
            _updateRequired = true;
            // locate a successor
            for (int i = sequence; i < ELEMENT_SEQUENCE.Length; i++)
            {
                XElement first = _page.Elements(_one.GetName(ELEMENT_SEQUENCE[i])).FirstOrDefault();
                if (first != null)
                {
                    first.AddBeforeSelf(element);
                    return;
                }
            }
            _page.AddFirst(element);
        }

        internal string[] PageTags
        {
            get
            {
                return _pageTagsOE != null ? ParseTags(_pageTagsOE.Value) : new string[0];
            }

            set
            {
                XName tagName = _one.GetName("Tag");
                
                // collect all tag definitions
                XName tagdefName = _one.GetName("TagDef");
                IEnumerable<XElement> tagDefs = _page.Elements(tagdefName);

                int nextTagDefIndex = tagDefs.Count(); // next index for creating new tags
 
                if (_pageTagsOE == null)
                {
                    _updateRequired = true;
                        
                    // Create a style for the tags - if needed
                    // <one:QuickStyleDef index="1" name="cite" fontColor="#595959" highlightColor="automatic" font="Calibri" fontSize="9"
                    XName styledefName = _one.GetName("QuickStyleDef");

                    IEnumerable<XElement> quickstyleDefs = _page.Elements(styledefName);

                    XElement tagStyle = quickstyleDefs.FirstOrDefault(d => d.Attribute("name").Value == Properties.Settings.Default.TagOutlineStyle_Name);

                    if (tagStyle == null)
                    {
                        tagStyle = new XElement(styledefName,
                                                new XAttribute("index",(quickstyleDefs.Count() + 1).ToString()),
                                                new XAttribute("name",Properties.Settings.Default.TagOutlineStyle_Name),
                                                new XAttribute("fontColor","#595959"),
                                                new XAttribute("highlightColor","automatic"),
                                                new XAttribute("bold", Properties.Settings.Default.TagOutlineStyle_Font.Bold.ToString().ToLower()),
                                                new XAttribute("italic", Properties.Settings.Default.TagOutlineStyle_Font.Italic.ToString().ToLower()),
                                                new XAttribute("font", Properties.Settings.Default.TagOutlineStyle_Font.Name),
                                                new XAttribute("fontSize",Properties.Settings.Default.TagOutlineStyle_Font.Size));
                        addElementToPage(tagStyle,QUICKSTYLEDEF_IDX);
                    }

                    // create tag definition for the tags outline like so:
                    // <one:TagDef index="3" name="Page Tags" type="23" symbol="26" />
                    XElement pageTagsDef = tagDefs.FirstOrDefault(t => t.Attribute("name").Value == "Page Tags");
                    if (pageTagsDef == null)
                    {
                        pageTagsDef = new XElement(tagdefName,
                                                   new XAttribute("index", nextTagDefIndex++),
                                                   new XAttribute("name", "Page Tags"),
                                                   new XAttribute("type", "23"),
                                                   new XAttribute("symbol", "26"));
                        _page.AddFirst(pageTagsDef);
                    }

                    // Create a outline for the page tags
                    //
                    //<one:Outline author="Peter Ernst" authorInitials="PE" lastModifiedBy="Peter Ernst" lastModifiedByInitials="PE" objectID="{E470786C-A904-4E9F-AC3B-0D9F36B6FC54}{14}{B0}" lastModifiedTime="2013-12-06T16:04:48.000Z">
                    //  <one:Position x="236.249984741211" y="42.1500015258789" z="0" />
                    //  <one:Size width="90.8847274780273" height="10.9862976074219" />
                    //  <one:OEChildren>
                    //    <one:OE objectID="{E470786C-A904-4E9F-AC3B-0D9F36B6FC54}{15}{B0}" lastModifiedTime="2013-12-06T16:04:48.000Z" quickStyleIndex="1" creationTime="2013-12-06T16:03:50.000Z">
                    //      <one:Tag index="0" completed="true" creationDate="2013-12-06T15:55:59.000Z" completionDate="2013-12-06T15:55:59.000Z" />
                    //      <one:T><![CDATA[Gdfgdf, sdfdsf]]></one:T>
                    //    </one:OE>
                    //  </one:OEChildren>
                    //</one:Outline>
                    XElement outline = new XElement(_one.GetName("Outline"),
                                                   new XElement(_one.GetName("Position"),
                                                                new XAttribute("x", "236"),
                                                                new XAttribute("y", "43"),
                                                                new XAttribute("z", "1")),
                                                   new XElement(_one.GetName("Size"),
                                                                new XAttribute("width", "210"),
                                                                new XAttribute("height", "10"),
                                                                new XAttribute("isSetByUser", "true")),
                                                   new XElement(_one.GetName("OEChildren"),
                                                 (_pageTagsOE = new XElement(_one.GetName("OE"),
                                                                             new XAttribute("quickStyleIndex", tagStyle.Attribute("index").Value),
                                                                             new XElement(tagName,
                                                                                          new XAttribute("index", pageTagsDef.Attribute("index").Value),
                                                                                          new XAttribute("completed", "true"))))));
                    _page.Add(outline);
                }

                // add the tags to the title tag
                // <one:Title lang="de">
                //  <one:OE objectID="{9A0ACA13-6D63-4137-8821-5D7C5408BB6C}{15}{B0}" lastModifiedTime="2013-12-08T14:08:11.000Z" quickStyleIndex="0" author="Peter Ernst" authorInitials="PE" lastModifiedBy="Peter Ernst" lastModifiedByInitials="PE" creationTime="2013-12-08T14:08:11.000Z">
                //    <one:Tag index="0" completed="true" creationDate="2013-12-06T20:31:43.000Z" completionDate="2013-12-06T20:31:43.000Z" />
                //    <one:Tag index="1" completed="true" creationDate="2013-12-06T20:34:05.000Z" completionDate="2013-12-06T20:34:05.000Z" />
                //    <one:Tag index="2" completed="true" creationDate="2013-12-06T20:35:41.000Z" completionDate="2013-12-06T20:35:41.000Z" />
                //    <one:T><![CDATA[Test Addin ]]></one:T>
                //  </one:OE>
                //</one:Title>
                XName titleName = _one.GetName("Title");
                XElement title = _page.Elements(titleName).FirstOrDefault();
                if (title == null)
                {
                    _updateRequired = true;
                    title = new XElement(titleName, new XElement(_one.GetName("OE")));
                    addElementToPage(title,TITLE_IDX);
                }

                XElement titleOE = title.Elements(_one.GetName("OE")).FirstOrDefault();

                // create or locate tag definitions for existing page tags and record their indices  
                // <one:TagDef index="0" name="Test Tag 1" type="0" symbol="0" />
                // <one:TagDef index="1" name="Test Tag 2" type="1" symbol="0" />
                IDictionary<string, string> tagToIndexMap = new Dictionary<string, string>();
                foreach (XElement tagdef in tagDefs.Where(d => d.Attribute("symbol").Value == "0"
                                                               && !string.IsNullOrEmpty(d.Attribute("fontColor").Value)
                                                               && System.Drawing.ColorTranslator.ToHtml(Properties.Settings.Default.PageTagDef_Fontcolor).ToLower().Equals(d.Attribute("fontColor").Value.ToLower())))
                {
                    tagToIndexMap[tagdef.Attribute("name").Value] = tagdef.Attribute("index").Value;
                    // make sure type is equal to index so that index is unique
                    tagdef.Attribute("type").Value = tagdef.Attribute("index").Value;
                }

                // add tag definition and tags, if needed
                foreach (string tag in value)
                {
                    string strIndex;
                    if (!tagToIndexMap.TryGetValue(tag, out strIndex))
                    { // create a new definition for this tag
                        _updateRequired = true;
                        strIndex = (nextTagDefIndex++).ToString();
                        XElement tagdef = new XElement(tagdefName,
                                                       new XAttribute("index", strIndex),
                                                       new XAttribute("name", tag),
                                                       new XAttribute("type", strIndex),
                                                       new XAttribute("fontColor", System.Drawing.ColorTranslator.ToHtml(Properties.Settings.Default.PageTagDef_Fontcolor)),
                                                       new XAttribute("symbol", "0"));
                        _page.AddFirst(tagdef);
                    }

                    XElement titleTag = titleOE.Elements(tagName).FirstOrDefault(t => t.Attribute("index").Value == strIndex);
                    
                    if (titleTag == null)
                    {
                        _updateRequired = true;
                        titleOE.AddFirst(new XElement(_one.GetName("Tag"),
                                                      new XAttribute("index", strIndex),
                                                      new XAttribute("completed", "true")));
                    }

                    tagToIndexMap.Remove(tag);
                }

                // remove unused tags from the title
                foreach (string strIndex in tagToIndexMap.Values)
                {
                    XElement tag = titleOE.Elements(tagName).FirstOrDefault(t => t.Attribute("index").Value == strIndex);
                    if (tag != null)
                    {
                        _updateRequired = true;
                        tag.Remove();
                    }
                }

                string strTags = string.Join(", ", value);

                // create the <one:Meta> element for page tags, if needed
                XName metaName = _one.GetName("Meta");
                XElement meta = _page.Elements(metaName).FirstOrDefault(m => m.Attribute("name").Value == META_NAME);
                if (meta == null)
                {
                    meta = new XElement(metaName,
                                        new XAttribute("name",META_NAME));
                    addElementToPage(meta,META_IDX);
                }
                
                meta.SetAttributeValue("content",strTags);

                if (!strTags.Equals(_pageTagsOE.Value))
                {
                    _updateRequired = true;
                    // remove all old <one:T> tags
                    XName tName = _one.GetName("T");
                    foreach (XElement t in _pageTagsOE.Elements(tName).ToArray())
                    {
                        t.Remove();
                    }
                    _pageTagsOE.Add(new XElement(tName, strTags));
                    // turn off spell checking
                    _pageTagsOE.SetAttributeValue("lang","yo");
                }
            }
        }

        /// <summary>
        /// Save all changes to the page to OneNote
        /// </summary>
        /// <remarks>This method can only be called once. To further edit the page
        /// a new instance of the page proxy must be created</remarks>
        internal void Update()
        {
            if (_updateRequired)
            {
                _onenote.UpdatePageContent(_pageDoc.ToString(),default(DateTime),_schema);
            }
            _onenote = null;
        }
    }
}
